/**
 * Copyright (C) 2011-2012 Luca Li Greci
 *
 * This file is part of pojo-creator
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *         http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.pojocreator.metamodel;

import java.lang.reflect.Field;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.Set;

import org.apache.commons.lang.StringUtils;

import com.pojocreator.utility.ReflectionHelper;

/**
 * This class keeps track of the relationship among the objects.
 * 
 * @author Luca Li Greci
 */
public class MetaModel {
    private Set < Class < ? > > visited;
    private Map < Class < ? >, List < MClass > > registry;
    private Class < ? > root;

    /**
     * Create the meta model starting from a root object.
     * 
     * @param root Root of object dependency tree.
     */
    public MetaModel(Class < ? > root) {
        visited = new HashSet < Class < ? > >();
        registry = new HashMap < Class < ? >, List < MClass > >();
        this.root = root;
        inspect(root);
    }

    /**
     * Return a {@link List} of all {@link MClass} discovered in the entire
     * dependency tree.
     * 
     * @return a {@link List} of all {@link MClass} discovered in the entire
     *         dependency tree..
     */
    public List < MClass > allModels() {
        return list();
    }

    /**
     * Return a {@link List} of all {@link MClass} discovered at the root level.
     * 
     * @return a {@link List} of all {@link MClass} discovered at the root
     *         level.
     */
    public List < MClass > models() {
        return list(root);
    }

    /**
     * Return a {@link List} of all {@link MClass} discovered at the given
     * {@link Class} as starting point.
     * 
     * @return a {@link List} of all {@link MClass} discovered at the given
     *         {@link Class} as starting point. level.
     */
    public List < MClass > models(Class < ? > clazz) {
        return list(clazz);
    }

    /**
     * Return a {@link MClass} using a property name at the root level.
     * 
     * @return a {@link MClass} using a property name at the root level.
     */
    public MClass model(String propertyName) {
        List < MClass > list = models();
        for (MClass mc : list) {
            if (StringUtils.equals(mc.getName(), propertyName)) {
                return mc;
            }
        }
        return null;
    }

    /**
     * 
     * @param propertyClass
     * @return
     */
    // TODO : change the return type to be MClass and thrown the AmbiguousException from here.
    public List < MClass > model(Class < ? > propertyClass) {
        List < MClass > filtered = new ArrayList < MClass >();
        List < MClass > list = models();
        for (MClass mc : list) {
            if (mc.getType() == propertyClass) {
                filtered.add(mc);
            }
        }
        return filtered;
    }

    private void register(Class < ? > key, MClass value) {
        List < MClass > values = list(key);
        values.add(value);
        registry.put(key, values);
    }

    private List < MClass > list(Class < ? > key) {
        List < MClass > values = registry.get(key);
        if (values == null) {
            return new ArrayList < MClass >();
        }
        return values;
    }

    private List < MClass > list() {
        List < MClass > out = new ArrayList < MClass >();
        Set < Entry < Class < ? >, List < MClass >>> entrySet = registry.entrySet();
        for (Entry < Class < ? >, List < MClass >> entry : entrySet) {
            out.addAll(entry.getValue());
        }
        return out;
    }

    private void inspect(Class < ? > root) {
        if (visited.contains(root)) {
            return;
        }
        Field[] declaredFields = root.getDeclaredFields();
        for (Field field : declaredFields) {
            Class < ? > type = field.getType();
            MClass model = new MClass();
            model.setName(field.getName());
            model.setBelongTo(root);
            model.setType(type);
            model.setMultiplicity(MType.SIMPLE);
            String packageName = null;

            if (!type.isPrimitive()) {
                Package pack = type.getPackage();
                if (pack != null) {
                    packageName = pack.getName();
                }

                if ("java.util".equals(packageName)) {
                    if ("List".equals(type.getSimpleName())) {
                        type = ReflectionHelper.collectionOf(field);
                        model.setMultiplicity(MType.LIST);
                        model.setType(type);
                    }
                } else if (type.isArray()) {
                    model.setMultiplicity(MType.ARRAY);
                }
            }

            register(root, model);

            if (!type.isEnum() && !type.isPrimitive() && !"java.lang".equals(packageName)) {
                inspect(type);
            }
            visited.add(root);
        }
    }

}
